package com.googlecode.vmock;

import java.math.BigDecimal;
import java.math.BigInteger;

import com.googlecode.vmock.MockBuilder.ResultBuilder;
import com.googlecode.vmock.exceptions.NoCallToMockException;
import com.googlecode.vmock.matchers.CompareMatcher;
import com.googlecode.vmock.matchers.DisjunctionMatcher;
import com.googlecode.vmock.matchers.EqualsMatcher;
import com.googlecode.vmock.matchers.Matcher;
import com.googlecode.vmock.matchers.NotMatcher;
import com.googlecode.vmock.matchers.TypeMatcher;
import com.googlecode.vmock.proxy.ProxyFactory;
import com.googlecode.vmock.utils.LogicOperator;
import com.googlecode.vmock.utils.Validate;

/**
 * The main and only class that exposes mocking capability to the end user.
 * 
 * @author marcin.jagus
 */
public abstract class VMock {

    /**
     * Makes a call which returned a given value to be expected in replaying stage.
     * 
     * @param returnValue A return value returned by a call which is to be expected
     * @return Mock builder
     */
    public static <T> ResultBuilder<T> expect(T returnValue) {
        return createBuilder();
    }

    /**
     * Expects last <b>void</b> call to be invoked in replaying stage.
     * 
     * @return Mock builder
     */
    public static ResultBuilder<Void> expectLastCall() {
        return createBuilder();
    }

    /**
     * Creates a builder for the last recorded call.
     * 
     * @return Mock builder
     */
    private static <T> ResultBuilder<T> createBuilder() {
        RecordedCall call = MockController.getInstance().getLastRecordedCall();
        if (call == null) {
            throw new NoCallToMockException(
                    "There is no call to mock. It may indicate that CallInterceptorAspect is not working. Check your AspectJ configuration.");
        }

        return new MockBuilder(call).new ResultBuilder<T>();
    }

    /**
     * Puts VMock into recording stage.
     * <p>
     * In this stage, end-user decides which calls should be mocked and what kind of values should they return.
     * <p>
     * Due to security concerns, calls made to methods from <code>com.googlecode.vmock</code> and <code>java.lang</code> packages cannot be mocked.
     * Also, only calls made from 'recordOwner' object will be mocked. All the other objects calling VMock API will be ignored.
     * 
     * @param recordOwner Object that will be using VMock API. Calls to VMock API from other objects will be ignored.
     */
    public static void record(Object recordOwner) {
        MockController.getInstance().record(recordOwner);
    }

    /**
     * Puts VMock into paused stage. Can only be called while VMock is in recordining stage.
     * <p>
     * Paused stage gives end-user the possibility to call {@link VMock#record(Object)} with a different record owner.
     */
    public static void pause() {
        MockController.getInstance().pauseRecording();
    }

    /**
     * Puts VMock into replaying stage. In this stage, tested code should invoke mocked calls expected number of times and those mocked calls should
     * also return results specified by end-user in recording stage.
     */
    public static void replay() {
        MockController.getInstance().replay();
    }

    /**
     * Verifies if all calls mocked in recording stage were invoked expected number of times in replaying stage. If this is not the case, an exception
     * will be thrown.
     */
    public static void verify() {
        MockController.getInstance().verify();
    }

    /**
     * Returns a dummy instance of a given class.
     * <ul>
     * <li>For interface class, an JDK proxy is returned. The proxy is an instance of a given interface.</li>
     * <li>For abstract class, a CGLIB proxy is returned. The proxy is an subclass of a given abstract class.</li>
     * <li>For array class, a zero-element array is returned.</li>
     * <li>For concrete class, an instance of this class is returned. The instance is created silently, by calling the {@link Object} constructor
     * only, and is therefore completely uninitialized.</li>
     * </ul>
     * This logic can be modified or changed completely by supplying custom {@link ProxyFactory} implementation to the
     * {@link ProxyFactory#setInstance(ProxyFactory)} method.
     * 
     * @param clazz Given class. Can be an interface, abstract class, array class or a concrete class.
     * @return Dummy instance of a given class
     */
    public static <T> T mock(Class<T> clazz) {
        return ProxyFactory.getInstance().createProxy(clazz);
    }

    /**
     * Always returns <code>null</code> coerced to a type given by 'clazz' parameter.
     * 
     * @param clazz {@link Class} object which decides the type of returned <code>null</code> value
     * @return Always <code>null</code>
     */
    public static <T> T as(Class<T> clazz) {
        return null;
    }

    /**
     * Returns a dummy object of a given type that is bound to a given matcher.
     * 
     * @param clazz {@link Class} object which decides the type of returned dummy object
     * @param matcher Given matcher
     * @return dummy object of a given type
     */
    public static <T> T match(Class<T> clazz, Matcher matcher) {
        T mock = mock(clazz);
        MockController.getInstance().registerMatcher(matcher, mock);
        return mock;
    }

    /**
     * Returns a dummy object of a given type that is bound to a {@link EqualsMatcher} constructed with a given value. The newly created matcher will
     * only accept values which are equal to a given value.
     * 
     * @param value Given value
     * @return dummy object of a given value's type
     */
    @SuppressWarnings("unchecked")
    public static <T> T eq(T value) {
        Validate.notNull(value, "value == null");
        return (T) match(value.getClass(), new EqualsMatcher(value));
    }

    /**
     * Returns a dummy object of {@link Object} type that is bound to an {@link EqualsMatcher} constructed with a <code>null</code> value. The newly
     * created matcher will only accept <code>null</code> value.
     * 
     * @return dummy object of {@link Object} type
     */
    public static Object isNull() {
        return isNull(Object.class);
    }

    /**
     * Returns a dummy object of a given type that is bound to a {@link EqualsMatcher} constructed with a <code>null</code> value. The newly created
     * matcher will only accept <code>null</code> value.
     * 
     * @param type Given type
     * @return dummy object of a given type
     */
    public static <T> T isNull(Class<T> type) {
        return match(type, new EqualsMatcher(null));
    }

    /**
     * Returns a dummy object of a given type that is bound to a {@link TypeMatcher} that matches against all objects of a given type.
     * 
     * @param clazz {@link Class} object which decides the type of returned dummy object
     * @return dummy object of a given type
     */
    public static <T> T isA(Class<T> clazz) {
        return match(clazz, new TypeMatcher(clazz));
    }

    /**
     * Returns a dummy integer that is bound to a {@link TypeMatcher} that matches against any {@link Integer} object.
     * 
     * @return dummy integer
     */
    public static Integer anyInteger() {
        return isA(Integer.class);
    }

    /**
     * Returns a dummy long that is bound to a {@link TypeMatcher} that matches against any {@link Long} object.
     * 
     * @return dummy long
     */
    public static Long anyLong() {
        return isA(Long.class);
    }

    /**
     * Returns a dummy short that is bound to a {@link TypeMatcher} that matches against any {@link Short} object.
     * 
     * @return dummy short
     */
    public static Short anyShort() {
        return isA(Short.class);
    }

    /**
     * Returns a dummy byte that is bound to a {@link TypeMatcher} that matches against any {@link Byte} object.
     * 
     * @return dummy byte
     */
    public static Byte anyByte() {
        return isA(Byte.class);
    }

    /**
     * Returns a dummy double that is bound to a {@link TypeMatcher} that matches against any {@link Double} object.
     * 
     * @return dummy double
     */
    public static Double anyDouble() {
        return isA(Double.class);
    }

    /**
     * Returns a dummy float that is bound to a {@link TypeMatcher} that matches against any {@link Float} object.
     * 
     * @return dummy float
     */
    public static Float anyFloat() {
        return isA(Float.class);
    }

    /**
     * Returns a dummy {@link String} that is bound to a {@link TypeMatcher} that matches against any {@link String} object.
     * 
     * @return dummy {@link String}
     */
    public static String anyString() {
        return isA(String.class);
    }

    /**
     * Returns a dummy character that is bound to a {@link TypeMatcher} that matches against any {@link Character} object.
     * 
     * @return dummy character
     */
    public static Character anyChar() {
        return isA(Character.class);
    }

    /**
     * Returns a dummy {@link BigInteger} that is bound to a {@link TypeMatcher} that matches against any {@link BigInteger} object.
     * 
     * @return dummy {@link BigInteger}
     */
    public static BigInteger anyBigInteger() {
        return isA(BigInteger.class);
    }

    /**
     * Returns a dummy {@link BigDecimal} that is bound to a {@link TypeMatcher} that matches against any {@link BigDecimal} object.
     * 
     * @return dummy {@link BigDecimal}
     */
    public static BigDecimal anyBigDecimal() {
        return isA(BigDecimal.class);
    }

    /**
     * Returns a dummy object of a given array's component type that is bound to a {@link DisjunctionMatcher} which matches against values specified
     * in a given array.
     * 
     * @param acceptedValues Array of values that will be accepted by {@link DisjunctionMatcher}
     * @return dummy object that has the same type as the component type of a given array
     */
    @SuppressWarnings("unchecked")
    public static <T> T disjunction(T... acceptedValues) {
        if (acceptedValues.length < 2) {
            throw new IllegalArgumentException("Disjunction requires at least 2 values specified.");
        }

        return match((Class<T>) acceptedValues.getClass().getComponentType(), new DisjunctionMatcher<T>(acceptedValues));
    }

    /**
     * Returns a dummy object of a given type that is bound to a {@link DisjunctionMatcher} which matches against values specified as arguments of
     * this method.
     * <p>
     * If you need to specify more values, use the {@link VMock#disjunction(Object...)} method.
     * 
     * @param a First value that will be accepted by {@link DisjunctionMatcher}
     * @param b Second value that will be accepted by {@link DisjunctionMatcher}
     * @return dummy object of a given type
     */
    @SuppressWarnings("unchecked")
    public static <T> T or(T a, T b) {
        return disjunction(a, b);
    }

    /**
     * Returns a dummy object of a given value's type that is bound to a {@link NotMatcher} which matches against all values being not equal to a
     * given value.
     * 
     * @param value Given value
     * @return dummy object of a given value's type
     */
    @SuppressWarnings("unchecked")
    public static <T> T not(T value) {
        return (T) match(value.getClass(), new NotMatcher(value));
    }

    /**
     * Returns a dummy object of a given value's type that is bound to a {@link CompareMatcher} which matches against all values that satisfy a given
     * logical operator against a given value.
     * 
     * @param op Given logical operator. Cannot be <code>null</code>.
     * @param value A given {@link Comparable} value. Cannot be <code>null</code>.
     * @return dummy object of a given value's type
     */
    @SuppressWarnings("unchecked")
    public static <T extends Comparable<T>> T cmp(LogicOperator op, T value) {
        return (T) match(value.getClass(), new CompareMatcher<T>(op, value));
    }

    /**
     * Returns a dummy object of a given value's type that is bound to a {@link CompareMatcher} which matches against all values lower than a given
     * value.
     * 
     * @param value Given value
     * @return dummy object of a given value's type
     */
    public static <T extends Comparable<T>> T lt(T value) {
        return cmp(LogicOperator.LESS_THAN, value);
    }

    /**
     * Returns a dummy object of a given value's type that is bound to a {@link CompareMatcher} which matches against all values lower or equal to a
     * given value.
     * 
     * @param value Given value
     * @return dummy object of a given value's type
     */
    public static <T extends Comparable<T>> T le(T value) {
        return cmp(LogicOperator.LESS_EQUAL, value);
    }

    /**
     * Returns a dummy object of a given value's type that is bound to a {@link CompareMatcher} which matches against all values equal (in the
     * {@link Comparable} sense) to a given value.
     * 
     * @param value Given value
     * @return dummy object of a given value's type
     */
    public static <T extends Comparable<T>> T eq(T value) {
        return cmp(LogicOperator.EQUAL, value);
    }

    /**
     * Returns a dummy object of a given value's type that is bound to a {@link CompareMatcher} which matches against all values not equal (in the
     * {@link Comparable} sense) to a given value.
     * 
     * @param value Given value
     * @return dummy object of a given value's type
     */
    public static <T extends Comparable<T>> T neq(T value) {
        return cmp(LogicOperator.NOT_EQUAL, value);
    }

    /**
     * Returns a dummy object of a given value's type that is bound to a {@link CompareMatcher} which matches against all values greater than a given
     * value.
     * 
     * @param value Given value
     * @return dummy object of a given value's type
     */
    public static <T extends Comparable<T>> T gt(T value) {
        return cmp(LogicOperator.GREATER_THAN, value);
    }

    /**
     * Returns a dummy object of a given value's type that is bound to a {@link CompareMatcher} which matches against all values greater or equal to a
     * given value.
     * 
     * @param value Given value
     * @return dummy object of a given value's type
     */
    public static <T extends Comparable<T>> T ge(T value) {
        return cmp(LogicOperator.GREATER_EQUAL, value);
    }
}
